<?php
/* 
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
*/
jimport('joomla.html.pagination');

/**
 * JDatePagination получает массив данных и разбивает их на разные интервалы времени по полю created вида "2001-02-11 16:40:03"
 *
 * @author chaporgin_a
 */
class VA_JDatePagination implements Iterator {
    private $allItems = array();
    private $from = null;
    private $till = null;
    private $total = null;
    private $cacheDirty = TRUE;
    private $cachedItemsToShow = null;
    private $cachedTotalItemsToShow = null;
    /**
     *
     * @var array массив ключей массива $cachedItemsToShow
     */
    private $itemsToShowKeys = null;
    /**
     * Используется чтобы объект реализовал итератор, фактически, ключ текущего элемента
     * @var integer индекс для массива itemsToShowKeys
     */
    private $currentItem = null;
    /**
     *
     * @var int направление для итератора
     */
    public $iteratorDirection = 2;
    /**
     *
     * @param JModel $model
     * @param int $total to speed up execution (sometimes it can help)
     */
    public function __construct(JModel $model, $total = null) {
        foreach($model->getData() as $element) {
            $this->allItems[strtotime($element->created)] = $element;
        }


        ksort($this->allItems);
        if(null !== $total) $this->total = $total;
        else $this->total = count($this->allItems);
    }
    /**
     *
     * @return boolean
     */
    protected  function cacheDirty() {
        return $this->cacheDirty;
    }
    protected function cleanCache() {
        $this->cachedItemsToShow = $this->cacheItemsToShow();
        $this->cachedTotalItemsToShow = count($this->cachedItemsToShow);
        $this->cacheDirty = FALSE;
    }
    /**
     *
     * @return array
     */
    protected function cacheItemsToShow() {
        $items = array();
        /* @var $value stdClass */
        foreach($this->allItems as $timestampCreated => &$value)
            if( $this->from <= $timestampCreated && $timestampCreated < $this->till )
                $items[$timestampCreated] = $value;
        $this->itemsToShowKeys = array_keys($items);
        $this->currentItem = 0;
        return $items;
    }
    /**
     *
     * @return array элементы которые надо показать
     */
    public function getItemsToShow() {
        $this->updateCacheIfNeeded();
        return $this->cachedItemsToShow;
    }
    /**
     *
     * @return int
     */
    public function countItemsToShow() {
        $this->updateCacheIfNeeded();
        return $this->cachedTotalItemsToShow;
    }
    protected function updateCacheIfNeeded() {
        if($this->cacheDirty()) $this->cleanCache();
    }
    /**
     * Чтобы установить диапазон
     * @param int $from timestamp, с какой даты начать показывать, включая
     * @param int $till timestamp, с какой даты перестать показывать, не включая
     */
    public function setRange($from, $till) {
        $this->from = $from;
        $this->till = $till;
        if( 0 === count($this->findElementsInRange($from, $till))) {
            $can = $this->getFirstDateAfterActiveRegion(self::DIR_DESC);

            if( null !== $can) {
                $this->from = strtotime(date('Y-m-01',$can));
                $this->till = strtotime(date('Y-m-01',strtotime('+1 month', $this->from)));
            }
        }
        $this->cacheDirty = TRUE;
    }
    /**
     * Чтобы не работать с некорректным диапазоном
     * @return bool
     */
    protected function rangeIsSet() {
        return (null === $this->from) || (null === $this->till);
    }
    /**
     * Если диапазон пуст
     */
    protected function setDefaultRange() {
        $this->from = strtotime(date('Y-m-01'));
        $this->till = strtotime('+1 month', strtotime($this->from));
    }
    /**
     * Возвращает true, если в указанном диапазоне есть материал
     * @param int $from timestamp, начало диапазона, включая
     * @param int $till timestamp, конец диапазона, не включая
     * @return boolean
     */
    public function hasMaterialInRange($from, $till) {
        foreach($this->allItems as $timestampCreated => $material) {
            if(($from <= $timestampCreated) && ($timestampCreated < $till)) return TRUE;
        }
        return FALSE;
    }
    /**
     * Iterator interface methods
     *
     * @return int index of element
     */
    public function key() {
        $this->updateCacheIfNeeded();
        return $this->itemsToShowKeys[$this->currentItem]; //key($this->cachedItemsToShow);
    }
    /**
     * Iterator interface methods
     * @return stdClass current element
     */
    public function current() {
        $this->updateCacheIfNeeded();
        return $this->getCurrentItemToShow(); //current($this->cachedItemsToShow);
    }
    protected function getCurrentItemToShow() {
        return $this->cachedItemsToShow[$this->itemsToShowKeys[$this->currentItem]];
    }

    /**
     * Return false  on failure
     * @return mixed
     */
    public function next() {
        $this->updateCacheIfNeeded();
        if(self::DIR_DESC === $this->iteratorDirection) {
            --$this->currentItem;
        } else {
            ++$this->currentItem;
        }
    }
    /**
     * Return true on success
     * @return mixed first value of array
     */
    public function rewind() {
        $this->updateCacheIfNeeded();
        $cnt = count($this->cachedItemsToShow);
        if(self::DIR_DESC === $this->iteratorDirection)
            $this->currentItem = $cnt - 1;
        else $this->currentItem = 0;
        //return reset($this->cachedItemsToShow);
    }
    /**
     * Returns true if got more articles during sequential iteration
     * @return bool
     */
    public function valid() {
        $this->updateCacheIfNeeded();
        if(count($this->itemsToShowKeys) === 0) return FALSE;
        if(self::DIR_DESC === $this->iteratorDirection)
            return 0 <= $this->currentItem;
        else return count($this->itemsToShowKeys) > $this->currentItem;
    }
    public function getFromTimestamp() {
        return $this->from;
    }
    public function getTillTimestamp() {
        return $this->till;
    }
    /**
     * Разбивает диапазон между начальной и конечной датой пагинатора на участки меньшие или равные $offsetUnit
     *
     * Чтобы печатать ленту месяцев или лет надо знать какие элементы в ней должны быть.
     * Эта функция решает эту задачу.
     *
     * @param string $offsetUnit (month, years) - что-нибудь такое, единица измерения в котором будет отображаться лента
     * @param int $startFrom unix timestamp
     * @param int $elementsBeforeRange сколько элементов показать перед нашим диапазоном
     * @param int $elementsAfterRange сколько элементов показать после нашего диапазона
     * @return array
     */
    public function prepareStopDates($offsetUnit, $startFrom, $elementsBeforeRange, $elementsAfterRange) {
        $result = array();
        // собрать даты до выбранного нами диапазона
        $this->loopInDirection($result, $elementsBeforeRange, strtotime('-1 '.$offsetUnit,$startFrom), $offsetUnit, self::DIR_DESC);
        $result[] = $this->getFromTimestamp();
        $this->loopInDirection($result, $elementsAfterRange, strtotime('+1 '.$offsetUnit,$startFrom), $offsetUnit, self::DIR_ASC);
        return $result;
    }
    /**
     * Функция для непосредственной нарезки временной шкалы на стоп даты
     *
     * @param array $result
     * @param int $howManyElementsToGet - сколько нужно отметить точек на временной шкале
     * @param int $from timestamp - с какого момента начинать нарезку
     * @param string $offsetUnit months, years - делящий отрезок
     * @param mixed $direction направление нарезки
     */
    protected function loopInDirection(&$result, $howManyElementsToGet, $from, $offsetUnit, $direction) {

        if(0 === $howManyElementsToGet) return;
        $offsetString = sprintf(self::DIR_ASC === $direction ? '+1 %s':'-1 %s',$offsetUnit);
        $till = strtotime('+1 '.$offsetUnit, $from);

        $datesLeft = $howManyElementsToGet;
        $stopCriteria = FALSE;
        while(FALSE === $stopCriteria) {
            reset($this->allItems);
            $stopCriteria = TRUE;
            while($element = current($this->allItems)) {
                $ts = key($this->allItems);
                if((($ts < $from) && (self::DIR_DESC === $direction)) ||
                        ((self::DIR_ASC === $direction) && ($ts >= $till)))
                    $stopCriteria = FALSE;// пока еще есть непроверенные диапазоны
                if(($from <= $ts) && ($ts < $till)) {// попали в заданный диапазон
                    if(self::DIR_ASC === $direction) array_push($result, $from);
                    else array_unshift($result, $from);
//                    $result[] = $from;
                    --$datesLeft;

                    if(TRUE === $stopCriteria) {
                        if($datesLeft > 0) $stopCriteria = FALSE;//нам нужно получить еще дат
                    } elseif(0 === $datesLeft) $stopCriteria = TRUE;
                    break;
                }
                next($this->allItems);
            }
            if(self::DIR_DESC === $direction) {
                $till = $from;
                $from = strtotime($offsetString, $from);
            } elseif (self::DIR_ASC === $direction) {
                $from = $till;
                $till = strtotime($offsetString, $till);
            }
        }
        // остались еще даты
        if(0 < $datesLeft) {
            if(self::DIR_ASC === $direction) {
                $till = $from;
                $from = strtotime('-1 '.$offsetUnit, $from);
            } else {
                $from = $till;
                $till = strtotime('+1 '.$offsetUnit, $till);
            }
            while(0 < $datesLeft--) {
                if(self::DIR_ASC === $direction) {
                    array_push($result, $from);
                    $till = $from;
                    $from = strtotime($offsetString, $from);
                }
                else {
                    array_unshift($result, $from);
                    $from = $till;
                    $till = strtotime($offsetString, $till);
                }
            }
        }
    }
    public function findElementsInRange($from, $till, $limit = 1) {
        $return = array();
        foreach($this->allItems as $timestamp => &$element)
            if(($from <= $timestamp) && ($timestamp < $till)) {
                $return[] = $element;
                if(0 === --$limit) break;
            }
        return $return;
    }
    /**
     * В направлении течения времени
     */
    const DIR_ASC = 1;
    /**
     * В направлении против течения времени
     */
    const DIR_DESC = 2;

    public function getFirstDateAfterActiveRegion($direction) {
        if(!in_array($direction, array(self::DIR_ASC,self::DIR_DESC)))
            throw new Exception('no such direction '.$direction);
        $result = null;
        $big = array_keys($this->allItems);
        $small = array_keys($this->getItemsToShow());
        $r = null;
        if(0 === count($small)) $r = $this->getFDAARIfSmallArrayEmpty($direction, $big);
        if(null !== $r) return $r;
        $bigCount = count($big);
        $smallCount =  count($small);
        if( $bigCount < 2) {
            return $result;
        }
        if(self::DIR_DESC === $direction) {
            foreach($big as $key => $value)
                if($value == $small[0]) {
                    if($key === 0) return $result;
                    else return $big[$key-1];
                }
        } else {
            for($i=$bigCount-2;$i>-1;--$i)
                if($big[$i] == $small[$smallCount-1]) {
                    return $big[$i+1];
                }
        }
        return $result;
    }
    /**
     * Нужно достать первую дату в заданном направлении при условии что нет элементов для показа в заданном регионе
     * @param mixed $direction
     */
    private function getFDAARIfSmallArrayEmpty($direction, &$big) {
        $cand = $big[0];
        if((self::DIR_DESC) && ($cand > $this->from)) return null;
        foreach ($big as $ts) {
            if(self::DIR_DESC) {
                if( $cand < $ts) $cand = $ts;
            } elseif($this->till < $ts) return $ts;
        }
        return $cand;
    }
    /**
     * Функция для очень больших массивов должна давать неплохие результаты
     * Нужна для нахождения произвольного элемента из
     *
     */
    private final function getFirstElementMethod($direction) {
        $result = null;
        $index = null;
        $bigCount = count($this->allItems);
        if( (0 === $bigCount) || (0 === count($this->getItemsToShow()))) return $result;
        $big = array_keys($this->allItems);
        $countBig = count($big);
        $small = array_keys($this->getItemsToShow());

        // найдем индекс массива $big для произвольного элемента из массива $small
        //сначала проверим крайние элементы $big
        $candidates = array(0 => $this->allItems[0], ($bigCount-1) => $this->allItems[$bigCount-1]);
        $found = FALSE;
        $iteration = 1;
        while(FALSE === $found) {
            foreach($candidates as $key => &$value) {
                if(in_array($value, $small)) {
                    $found = TRUE;
                    $index = $key;
                    break;
                }
            }
            $candidates = $this->produceCandidates($big, $iteration++, $countBig);
        }
        if(null === $index) throw new Exception('big index not found??');
        //узнаем номер этого же элемента в массиве small
        $smIn = null;
        foreach($small as $key => &$value)
            if( $value == $big[$index] ) {
                $smIn = $key;
                break;
            }
        if(null === $smIn) throw new Exception('small index not found??');
        if($direction === self::DIR_DESC) {
            if($index > $smIn) return $big[$index-$smIn-1];
        } else {
            if($bigCount > $index + count($small) - $smIn) return $big[$index + count($small) - $smIn];
        }
        return $result;
    }
    /**
     * Помогает реализовать метод деления отрезка пополам, выдает кандидатов на попадание в заданное множество
     *
     * Вообщем по идее прирост скорости будет давать только когда целевое множество элементов довольно большое
     * а не в десятки раз меньше общего количества элементов
     * @param array $pretendents целевой отрезок
     * @param int $number - номер итерации деления
     */
    private function produceCandidates(&$pretendents, $number, $numberOfPretendents) {
        $indexes = array();
        $step = round($numberOfPretendents / ($number << $number));
        $omit = TRUE;
        $index = $step;
        $twiceStep = 2*$step;//для скорости
        while($index < $numberOfPretendents) {
            $indexes[$index] = $pretendents[$index];
            $index += $twiceStep;
        }
        return $indexes;
    }
//    private function checkInSmallArray(&$)
}

